package com.txtsqlclient.ws;

import java.io.IOException;
import java.sql.Connection;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import Acme.Serve.ServeSessionManager;

import com.txtsqlclient.dataaccess.database.QueryExecutionDAO;
import com.txtsqlclient.dataaccess.database.connection.ConnectionException;
import com.txtsqlclient.dataaccess.database.connection.ConnectionFactory;
import com.txtsqlclient.domain.DataSource;
import com.txtsqlclient.domain.QueryExecutionException;
import com.txtsqlclient.domain.QueryResult;
import com.txtsqlclient.domain.ReadQueryResult;
import com.txtsqlclient.domain.WriteQueryResult;
import com.txtsqlclient.ui.vo.Query;

@SuppressWarnings("serial")
public class QueryServlet extends BaseServlet {

	public static final String START_INDEX_INVALID_MSG = "Start Index is not a number";
	public static final String PAGE_SIZE_INVALID_MSG = "PageSize is not a number";
	public static final String PAGE_SIZE_EMPTY_MSG = "PageSize is empty";
	public static final String QUERY_INVALID_MSG = "Query is empty";
	public static final String PAGE_SIZE_PARAM = "pageSize";
	public static final String QUERY_PARAM = "query";
	public static final String START_INDEX_PARAM = "startIndex";
	public static final String QUERY_SESSION_ATTRIBUTE = "Query";
	public static final String DATA_SOURCE_SESSION_VARIABLE = "DataSource";
	private static Log LOGGER = LogFactory.getLog(QueryServlet.class);

	protected void processRequest(HttpServletRequest req,
			HttpServletResponse res) throws IOException {
		LOGGER.info("Request received for Query at: "
				+ DateFormat.getDateTimeInstance().format(
						Calendar.getInstance().getTime()));
		Query query = null;
		try {
			query = validate(req, res);
		} catch (ValidationException e) {
			populateErrorResponse(req, res, e,
					"Error validating query information", query);
			LOGGER.error("Error validating query information", e);
			return;
		}
		HttpSession session = req.getSession();
		Query oldQuery = (Query) session
				.getAttribute(QueryServlet.QUERY_SESSION_ATTRIBUTE);
		if (oldQuery != null && oldQuery.getQuery().equals(query.getQuery())) {
			// Pagination Request
			oldQuery.setPageSize(query.getPageSize());
			oldQuery.setStartIndex(query.getStartIndex());
			try {
				populateReadResponse(req, res, oldQuery);
			} catch (QueryExecutionException e) {
				populateErrorResponse(req, res, e,
						"Error while reading Query results.", query);
				LOGGER.error("Error while reading results for query: " + query,
						e);
			}
		} else {
			DataSource ds = (DataSource) session
					.getAttribute(QueryServlet.DATA_SOURCE_SESSION_VARIABLE);
			QueryResult result = null;
			if (ds != null) {
				try {
					Connection con = ConnectionFactory.getInstance()
							.getConnection(ds);
					QueryExecutionDAO queryDAO = QueryExecutionDAO
							.getInstance();
					result = queryDAO.execute(query.getQuery(), con);
					if (result != null) {
						if (result instanceof ReadQueryResult) {
							query.setQueryResults((ReadQueryResult) result);
							populateReadResponse(req, res, query);
							session
									.setAttribute(
											QueryServlet.QUERY_SESSION_ATTRIBUTE,
											query);
						} else if (result instanceof WriteQueryResult)
							populateWriteResponse(req, res,
									(WriteQueryResult) result, query);
					}
					// what if result is null? when can result be null?
				} catch (ConnectionException e) {
					populateErrorResponse(req, res, e,
							"Database connection cannot be opened.", query);
					LOGGER.error("Database connection cannot be opened.", e);
				} catch (QueryExecutionException e) {
					populateErrorResponse(
							req,
							res,
							e,
							"There was an error while executing the query. Please check your query and the database connection.",
							query);
					LOGGER.error("Error executing query:" + query, e);
				}
			} else {
				populateErrorResponse(
						req,
						res,
						null,
						"Database connection cannot be found. Your session could have expired. Please login again.",
						query);
			}
		}
	}

	/**
	 * This method is required to populate and validate the Query details from
	 * the HTTP request
	 * 
	 * @param req
	 *            The HTTP Request object
	 * @param res
	 *            The HTTP Response object
	 * @return a Query object populate with the parameters present in the HTTP
	 *         request
	 * @throws ValidationException
	 *             if any of the required fields are not present in the HTTP
	 *             request
	 */
	protected Query validate(HttpServletRequest req, HttpServletResponse res)
			throws ValidationException {
		Query query = new Query();
		String queryStr = req.getParameter(QueryServlet.QUERY_PARAM);
		String pageSize = req.getParameter(QueryServlet.PAGE_SIZE_PARAM);
		String startIndex = req.getParameter(QueryServlet.START_INDEX_PARAM);
		List<String> errors = new ArrayList<String>();
		if (queryStr == null || queryStr.trim().length() == 0) {
			errors.add(QueryServlet.QUERY_INVALID_MSG);
		}
		if (pageSize == null || pageSize.trim().length() == 0) {
			errors.add(QueryServlet.PAGE_SIZE_EMPTY_MSG);
		} else if (!pageSize.matches("[0-9]+")) {
			errors.add(QueryServlet.PAGE_SIZE_INVALID_MSG);
		}
		if (startIndex != null) {
			if (startIndex.matches("[0-9]+")) {
				query.setStartIndex(Integer.parseInt(startIndex));
			} else {
				errors.add(QueryServlet.START_INDEX_INVALID_MSG);
			}
		} else {
			query.setStartIndex(1);
		}
		if (!errors.isEmpty()) {
			ValidationException ve = new ValidationException(
					"Some required fields have not been filled");
			ve.addValidationErrors(errors);
			throw ve;
		}
		query.setQuery(queryStr);
		query.setPageSize(Integer.parseInt(pageSize));
		return query;
	}

	protected void populateErrorResponse(HttpServletRequest req,
			HttpServletResponse res, Throwable error, String errorMessage,
			Query query) throws IOException {
		res.setStatus(HttpServletResponse.SC_OK);
		res.setContentType("text/xml");
		ServletOutputStream out = res.getOutputStream();
		out
				.println("<p:QueryResponse xmlns:p='http://code.google.com/p/txt-sql-client' "
						+ ServeSessionManager.SESSION_COOKIE_NAME
						+ "='"
						+ req.getSession().getId() + "'>");
		out.println("<Query>" + query + "</Query>");
		out.println("<Errors><Error>" + errorMessage + "</Error>");
		Throwable t = error;
		while (t != null) {
			out.println("<Error>" + t.getMessage() + "</Error>");
			t = t.getCause();
		}
		if (error instanceof ValidationException) {
			for (String valErr : ((ValidationException) error)
					.getValidationErrors()) {
				out.println("<Error>" + valErr + "</Error>");
			}
		}
		out.println("</Errors></p:QueryResponse>");
		out.flush();
		out.close();
	}

	protected void populateReadResponse(HttpServletRequest req,
			HttpServletResponse res, Query query) throws IOException,
			QueryExecutionException {
		res.setStatus(HttpServletResponse.SC_OK);
		res.setContentType("text/xml");
		ServletOutputStream out = res.getOutputStream();
		List<String> header = query.getQueryResults().getHeader();
		List<List<String>> data = query.getQueryResults().getNextPage(
				query.getStartIndex(), query.getPageSize());
		out
				.println("<p:QueryResponse xmlns:p='http://code.google.com/p/txt-sql-client' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='http://code.google.com/p/txt-sql-client Query.xsd' "
						+ ServeSessionManager.SESSION_COOKIE_NAME
						+ "='"
						+ req.getSession().getId() + "'>");
		out.println("<Query>" + query.getQuery() + "</Query>");
		out.println("<ReadResult><Columns>");
		int i = 1;
		for (String col : header) {
			out.println("<Column id='" + (i++) + "'>" + col + "</Column>");
		}
		out.println("</Columns><Records>");
		out.println("<TotalRows>"
				+ query.getQueryResults().getNumOfRowsReturned()
				+ "</TotalRows>");
		out.println("<RowsReturned><From>" + query.getStartIndex() + "</From>");
		int to = query.getStartIndex() + query.getPageSize() - 1;
		to = to <= query.getQueryResults().getNumOfRowsReturned() ? to : query
				.getQueryResults().getNumOfRowsReturned();
		out.println("<To>" + to + "</To></RowsReturned>");
		for (List<String> row : data) {
			out.println("<Record>");
			i = 1;
			for (String item : row) {
				// Better not to concatenate the strings here, as it runs in a
				// loop.
				out.println("<Data ColumnId='" + (i++) + "'>" + item
						+ "</Data>");
			}
			out.println("</Record>");
		}
		out.println("</Records></ReadResult>");
		out.println("</p:QueryResponse>");
		out.flush();
		out.close();
	}

	protected void populateWriteResponse(HttpServletRequest req,
			HttpServletResponse res, WriteQueryResult result, Query query)
			throws IOException {
		res.setStatus(HttpServletResponse.SC_OK);
		res.setContentType("text/xml");
		ServletOutputStream out = res.getOutputStream();
		out
				.println("<p:QueryResponse xmlns:p='http://code.google.com/p/txt-sql-client' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='http://code.google.com/p/txt-sql-client Query.xsd' "
						+ ServeSessionManager.SESSION_COOKIE_NAME
						+ "="
						+ req.getSession().getId() + ">");
		out.println("<Query>" + query + "</Query>");
		out.println("<WriteResult><ModifiedRows>");
		out.println(result.getNumOfRowsModified());
		out.println("</ModifiedRows></WriteResult>");
		out.println("</p:QueryResponse>");
		out.flush();
		out.close();
	}
}
